import Watcher from "./observe/watcher";
import { createElementVNode, createTextVNode, isSameVnode } from "./vdom/index";

function createElm(vnode) {
  let { tag, data, children, text } = vnode;

  if (typeof tag === "string") {
    // 是字符串，创建的是标签。将真实节点和虚拟节点对应起来，后续如果修改属性了，可通过虚拟节点找到真实节点
    vnode.el = document.createElement(tag);

    // 处理元素的属性
    patchProps(vnode.el, {}, data);

    // 处理儿子，通过递归的方式；递归创建完后要把它塞到该元素内部
    children.forEach((element) => {
      vnode.el.appendChild(createElm(element));
    });
  } else {
    // 不是字符串，创建的是文本
    vnode.el = document.createTextNode(text);
  }

  return vnode.el;
}

function patchProps(el, oldProps = {}, props = {}) {
  // 老的属性中有新的没有，要删除老的
  let oldStyles = oldProps.style || {};
  let newStyles = props.style || {};
  // 老的样式中有新的吗？没有则删除
  for (const key in oldStyles) {
    if (!newStyles[key]) {
      el.style[key] = "";
    }
  }

  // 老的属性中有新的吗？没有则删除
  for (const key in oldProps) {
    if (!props[key]) {
      el.removeAttribute(key);
    }
  }

  // 用新的覆盖老的
  for (const key in props) {
    if (key === "style") {
      for (const styleName in props.style) {
        el.style[styleName] = props.style[styleName];
      }
    } else {
      el.setAttribute(key, props[key]);
    }
  }
}

function patch(oldVnode, vnode) {
  // 写的是初渲染流程
  const isRealElement = oldVnode.nodeType;

  if (isRealElement) {
    // 是真实dom节点
    const elm = oldVnode; // 获取真实元素
    const parentElm = elm.parentNode; // 拿到父元素

    let newElm = createElm(vnode);

    // 先把新的放到老旧节点下面，然后再删除老旧节点
    parentElm.insertBefore(newElm, elm.nextSibling);
    parentElm.removeChild(elm);

    return newElm;
  } else {
    // 是虚拟dom元素，考虑几种情况
    // 1.两个节点不是同一个节点，直接删除老的换上新的（没有比对）
    // 2.同一个节点（判断节点的tag和key）比较两个几点是否有差异（复用老节点）
    // 3.节点比较完毕后比较两人的孩子
    if (isSameVnode(oldVnode, vnode)) {
      // 用老父亲的节点进行替换
      let el = createElm(vnode);
      oldVnode.el.parentNode.replaceChild(el, oldVnode.el);
      return el;
    }

    // 文本的情况，文本我们期望比较一下内容
    let el = (vnode.el = oldVnode.el); // 复用老节点的元素
    if (!oldVnode.tag) {
      if (oldVnode.text !== vnode.text) {
        el.textContent = vnode.text; // 新文本覆盖老文本
      }
    }

    // 标签的情况，比对标签属性
    patchProps(el, oldVnode.data, vnode.data);
  }

  // 比较儿子节点，比较的时候需要判断双方是否有儿子
  patchVnode(oldVnode, vnode);

  return el;
}

function patchVnode(oldVnode, vnode) {
  let oldChildren = oldVnode.children || [];
  let newChildren = vnode.children || [];
  if (oldChildren.length > 0 && newChildren.length > 0) {
    // 完整的diff算法，需要比较二个人的儿子
    updateChildren(el, oldChildren, newChildren);
  } else if (newChildren.length > 0) {
    // 没有老的，有新的
    mountChildren(el, newChildren);
  } else if (oldChildren.length > 0) {
    // 没有新的，有老的，直接删除即可
    el.innerHTML = "";
  }
}

// 把新的儿子节点给老的父节点
function mountChildren(el, newChildren) {
  for (let i = 0; i < newChildren.length; i++) {
    const child = newChildren[i];
    el.appendChild(createElm(child));
  }
}

function updateChildren(el, oldChildren, newChildren) {
  // 针对数组的方法（push、pop、sort、unshift、shift）做优化。Vue2采取双指针，比较两个节点
  let oldStartIndex = 0;
  let newStartIndex = 0;
  let oldEndIndex = oldChildren.length - 1;
  let newEndIndex = newChildren.length - 1;

  let oldStartVnode = oldChildren[0];
  let newStartVnode = newChildren[0];

  let oldEndVnode = oldChildren[oldEndIndex];
  let newEndVnode = newChildren[newEndIndex];

  // 配置一一对应的映射表
  function makeIndexByKey(children) {
    let map = {};
    children.forEach((child, index) => {
      map[child.key] = index;
    });
    return map;
  }
  let map = makeIndexByKey(oldChildren);

  while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    if (!oldStartVnode) {
      oldStartVnode = oldChildren[++oldStartIndex];
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIndex];
    }
    // 双方有一方头指针大于尾部指针，则停止循环（有一方不满足则停止，|| 有一方满足则为true，继续执行）
    else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 相同节点，递归比较子节点
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldChildren[++oldStartIndex];
      newStartVnode = newChildren[++newStartIndex];

      // 比较开头节点
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 相同节点，递归比较子节点
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIndex];
      newEndVnode = newChildren[--newEndIndex];

      // 比较开头节点
    } else if (isSameVnode(oldEndVnode, newStartVnode)) {
      // 交叉对比 abcd -> dabc
      patchVnode(oldEndVnode, newStartVnode);
      // 将旧的尾部节点移动到旧的头部去。insertBefore具有移动性，会将原来的元素移动走
      el.insertBefore(oldEndVnode.el, oldStartVnode.el);

      oldEndVnode = oldChildren[--oldEndIndex];
      newStartVnode = newChildren[++newStartIndex];
    } else if (isSameVnode(oldStartVnode, newEndVnode)) {
      // 交叉对比 abcd -> dabc
      patchVnode(oldStartVnode, newEndVnode);
      // 将旧的尾部节点移动到旧的头部去。insertBefore具有移动性，会将原来的元素移动走
      // nextSibling如果没写，则会插入错误，如 abcd -> dcba 时，a会插到d前面
      el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);

      oldStartVnode = oldChildren[++oldStartIndex];
      newEndVnode = newChildren[--newEndIndex];
    }
    // 乱序比对，根据老的列表做映射关系，用新的去寻找。找得到则移动，找不到则添加，多余的就删除
    // 找到需要移动的节点索引
    let moveIndex = map[newStartVnode.key];
    if (moveIndex !== undefined) {
      // 找到对应的虚拟节点复用
      let moveVnode = oldChildren[moveIndex];

      el.insertBefore(moveVnode.el, oldStartVnode.el);
      oldChildren[moveIndex] = undefined; // 表示当前节点已被移走
      patchVnode(moveVnode, newStartVnode); // 比对属性和子节点
    } else {
      // 如果找不到，说明是新的，插入到老节点前面
      el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
    }

    newStartVnode = newChildren[++newStartIndex];
  }

  if (newStartIndex <= newEndIndex) {
    // 新的多了，多余的插入进去
    for (let i = newStartIndex; i < newEndIndex; i++) {
      let childEl = createElm(newChildren[i]);

      // 这里可能向后追加，也有可能是向前添加
      let anchor = newChildren[newEndIndex + 1]
        ? newChildren[newEndIndex + 1].el
        : null;
      // el.appendChild(childEl);
      anchor为null是会被认为是appendChild;
      el.insertBefore(childEl, anchor);
    }
  }

  if (oldStartIndex <= oldEndIndex) {
    // 老的多了，需要删除老的
    for (let i = oldStartIndex; i < oldEndIndex; i++) {
      if (!oldChildren[i]) {
        let childEl = oldChildren[i].el;
        el.removeChild(childEl);
      }
    }
  }
}

export const initLifeCycle = (Vue) => {
  Vue.prototype._update = function (vnode) {
    // 将vnode转换为真实dom
    const vm = this;
    const el = vm.$el;

    // patch既有初始化功能，又有更新的功能
    vm.$el = patch(el, vnode);
  };

  Vue.prototype._c = function () {
    return createElementVNode(this, ...arguments);
  };
  Vue.prototype._v = function () {
    return createTextVNode(this, ...arguments);
  };

  Vue.prototype._s = function (value) {
    if (typeof value === "object") return;
    return JSON.stringify(value);
  };

  Vue.prototype._render = function () {
    const vm = this;

    // 让with中的this指向vm。当渲染的时候会去实例中取值，就可以将属性和视图绑定在一起
    return vm.$options.render.call(vm); // 通过ast语法转义后的render方法
  };
};

export const mountComponent = (vm, el) => {
  // 这里的el是通过 querySelector处理过的
  vm.$el = el;

  const updateComponent = () => {
    vm._update(vm._render());
  };
  const a = new Watcher(vm, updateComponent, true);

  // 1.调用render方法产生虚拟节点 虚拟dom
  vm._update(vm._render());

  // 2.根据虚拟DOM产生真实dom

  // 3.插入到el元素中
};

// Vue核心流程：
// 1.创造响应式数据
// 2.模板转换成ast语法树
// 3.将ast语法树转换了render函数
// 4.后续每次数据更新可以只执行render函数（无需再执行ast转化过程
// render会产生虚拟节点（响应式数据）。根据生成的虚拟节点创造真实dom
